home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / local / bin / gpgApplet < prev    next >
Encoding:
Text File  |  2013-01-06  |  31.4 KB  |  961 lines

  1. #!/usr/bin/perl
  2.  
  3. =head1 NAME
  4.  
  5. gpgApplet - GNOME applet for OpenGPG text encryption
  6.  
  7. =head1 DESCRIPTION
  8.  
  9. gpgApplet allows encrypting and decrypting the content of the
  10. clipboard with a symmetric cipher using a passphrase. This is
  11. a graphical frontend on top of GnuPG.
  12.  
  13. Asymmetric decryption and clipboard verification are also supported.
  14.  
  15. =head1 PREREQUISITES
  16.  
  17. gpgApplet does not handle passphrase input. Since it also does not
  18. offer terminal interaction unless explicitly run from there, it relies
  19. in practice on some kind of GnuPG agent such as pinentry, Seahorse 2.x
  20. or GNOME keyring 3.x to manage passphrase input.
  21.  
  22. =cut
  23.  
  24. use strict;
  25. use warnings FATAL => 'all';
  26. use 5.10.0;
  27.  
  28. # VERSION
  29.  
  30. use Glib qw{TRUE FALSE};
  31. use Gtk2 qw{-init};
  32. use Gtk2::Gdk::Keysyms;
  33. use Gtk2::SimpleList;
  34.  
  35. use Encode qw{decode encode find_encoding};
  36. use English;
  37. use gpgApplet::GnuPG::Interface;
  38. use IO::Handle;
  39. use I18N::Langinfo qw{langinfo CODESET};
  40. use List::MoreUtils qw{none};
  41. use Switch;
  42. use DateTime;
  43.  
  44. use Locale::gettext;
  45. use POSIX;
  46. setlocale(LC_MESSAGES, "");
  47. textdomain("gpgApplet");
  48.  
  49.  
  50. =head1 GLOBALS
  51.  
  52. =cut
  53.  
  54. use constant C_SELECT      => 0;
  55. use constant C_NAME        => 1;
  56. use constant C_KEYID       => 2;
  57. use constant C_STATUS      => 3;
  58. use constant C_FINGERPRINT => 4;
  59. use constant C_USERIDS     => 5;
  60. use constant C_TRUSTED     => 6;
  61. use constant VISIBLE_COLS  => (C_NAME, C_KEYID, C_STATUS);
  62. use constant HIDDEN_COLS   => (C_FINGERPRINT, C_USERIDS, C_TRUSTED);
  63.  
  64. use constant COMBO_NAME        => 0;
  65. use constant COMBO_KEYID       => 1;
  66. use constant COMBO_FINGERPRINT => 2;
  67. use constant COMBO_ROLE        => 3;
  68.  
  69. my $gnupg         = gpgApplet::GnuPG::Interface->new();
  70. my $codeset       = langinfo(CODESET());
  71. my $encoding      = find_encoding($codeset);
  72. my $main_window   = Gtk2::Window->new();
  73. my $icon_factory  = Gtk2::IconFactory->new();
  74. # Set always_trust since GnuPG otherwise will fail if the key's
  75. # trust hasn't been set.
  76. my %gnupg_options = (armor => 1, always_trust => 0, meta_interactive => 0);
  77.  
  78. my $pgp_encrypted_msg = {
  79.     type   => 'message',
  80.     header => '-----BEGIN PGP MESSAGE-----',
  81.     footer => '-----END PGP MESSAGE-----'
  82. };
  83. my $pgp_signed_msg = {
  84.     type   => 'signed',
  85.     header => '-----BEGIN PGP SIGNED MESSAGE-----',
  86.     middle => '-----BEGIN PGP SIGNATURE-----',
  87.     footer => '-----END PGP SIGNATURE-----'
  88. };
  89. my @pgp_headers = ($pgp_encrypted_msg, $pgp_signed_msg);
  90.  
  91. =head1 MAIN
  92.  
  93. =cut
  94.  
  95. my $statusicon = build_statusicon();
  96. $statusicon->set_visible(TRUE);
  97. init_freshest_clipboard();
  98. init_icons_stock($icon_factory);
  99. detect_received(freshest_clipboard());
  100. Gtk2->main;
  101.  
  102.  
  103. =head1 FUNCTIONS
  104.  
  105. =cut
  106.  
  107. sub all_clipboards {
  108.     map {
  109.         Gtk2::Clipboard->get($_)
  110.     } (
  111.         Gtk2::Gdk->SELECTION_CLIPBOARD,
  112.         Gtk2::Gdk->SELECTION_PRIMARY
  113.     );
  114. }
  115.  
  116. {
  117.     my $freshest_clipboard;
  118.  
  119.     sub init_freshest_clipboard {
  120.         $freshest_clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
  121.     }
  122.  
  123.     sub freshest_clipboard {
  124.         return $freshest_clipboard;
  125.     }
  126.  
  127.     sub set_freshest_clipboard {
  128.         $freshest_clipboard = shift;
  129.     }
  130. }
  131.  
  132. sub build_statusicon {
  133.     my $icon = Gtk2::StatusIcon->new;
  134.     $icon->set_visible(FALSE);
  135.     $icon->set_from_icon_name('seahorse');
  136.     $icon->set_tooltip($encoding->decode(gettext('OpenPGP encryption applet')));
  137.  
  138.     my $menu   = Gtk2::Menu->new;
  139.     my $mexit  = Gtk2::MenuItem->new($encoding->decode(gettext('Exit')));
  140.     $mexit->signal_connect('activate' => sub { Gtk2->main_quit; });
  141.     my $mabout = Gtk2::MenuItem->new($encoding->decode(gettext('About')));
  142.     $mabout->signal_connect('activate' => sub { Gtk2->show_about_dialog(
  143.         $main_window,
  144.         'program-name' => 'gpgApplet',
  145.         'license'      => q{This program is free software; you can redistribute it and/or modify it under the terms of either:
  146.  
  147. a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or
  148.  
  149. b) the "Artistic License" which comes with Perl.
  150. },
  151.         'wrap-license' => 1,
  152.         'website'      => 'https://tails.boum.org/',
  153.     )});
  154.     $menu->append($mexit);
  155.     $menu->append(Gtk2::SeparatorMenuItem->new);
  156.     $menu->append($mabout);
  157.  
  158.     $icon->signal_connect('popup-menu', sub {
  159.         my $ticon = shift;
  160.         my $event = shift;
  161.         my $time = shift;
  162.         $menu->show_all;
  163.         $menu->popup(undef, undef, undef, undef, $event, $time);
  164.     });
  165.  
  166.     $icon->signal_connect('button-press-event' => sub {
  167.         my $ticon = shift;
  168.         my $event = shift;
  169.         return unless $event->button == 1;
  170.         my $action_menu = build_action_menu();
  171.         $action_menu->show_all;
  172.         $action_menu->popup(undef, undef, undef, undef, $event->button, $event->time);
  173.     });
  174.  
  175.     foreach (all_clipboards()) {
  176.         $_->signal_connect("owner-change" => sub {
  177.             my $clipboard = shift;
  178.             my $event     = shift;
  179.             handle_clipboard_owner_change($clipboard);
  180.         });
  181.     }
  182.  
  183.     return $icon;
  184. }
  185.  
  186. sub build_action_menu {
  187.     my $action_menu = Gtk2::Menu->new;
  188.  
  189.     my $text_type = detect_text_type(get_validated_clipboard_text());
  190.  
  191.     if ($text_type eq 'text' or $text_type eq 'none') {
  192.         my $msymencrypt    = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('Encrypt Clipboard with _Passphrase')));
  193.         $msymencrypt->signal_connect('activate' => sub { operate_on_clipboard(\&symmetric_encrypt, ['text']); });
  194.         $action_menu->append($msymencrypt);
  195.         my $msignencrypt    = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('Sign/Encrypt Clipboard with Public _Keys')));
  196.         $msignencrypt->signal_connect('activate' => sub { operate_on_clipboard(\&public_crypto, ['text']); });
  197.         $action_menu->append($msignencrypt);
  198.     }
  199.     if ($text_type eq 'message' or $text_type eq 'signed') {
  200.         my $mdecryptver = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('_Decrypt/Verify Clipboard')));
  201.         $mdecryptver->signal_connect('activate' => sub { operate_on_clipboard(\&decrypt_verify, ['message', 'signed']); });
  202.         $action_menu->append($mdecryptver);
  203.     }
  204.     my $mmanage = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('_Manage Keys')));
  205.     $mmanage->signal_connect('activate' => sub { manage_keys(); });
  206.     $action_menu->append($mmanage);
  207.  
  208.     return $action_menu;
  209. }
  210.  
  211. sub manage_keys {
  212.     system("seahorse &");
  213. }
  214.  
  215. sub all_text_types {
  216.     map { $_->{type} } @pgp_headers;
  217. }
  218.  
  219. sub detect_text_type {
  220.     my $text = shift;
  221.  
  222.     unless (defined $text && length($text)) {
  223.         return 'none';
  224.     }
  225.  
  226.     foreach (@pgp_headers) {
  227.         my $header = $_->{header};
  228.         my $footer = $_->{footer};
  229.         return $_->{type} if $text =~ m{$header.*$footer}ms;
  230.     }
  231.  
  232.     return 'text';
  233. }
  234.  
  235. sub text_is_of_type {
  236.     my $text        = shift;
  237.     my @valid_types = @_;
  238.  
  239.     my $text_type = detect_text_type($text);
  240.     if (none { $_ eq $text_type } @valid_types) {
  241.         return (
  242.             0,
  243.             $encoding->decode(
  244.                 gettext("The clipboard does not contain valid input data."))
  245.         );
  246.     }
  247.     return (1);
  248. }
  249.  
  250. sub get_validated_clipboard_text {
  251.     my $args = shift;
  252.     my @valid_types;
  253.  
  254.     if (exists $args->{valid_types} && defined $args->{valid_types}) {
  255.         @valid_types = @{ $args->{valid_types} };
  256.     }
  257.     else {
  258.         @valid_types = all_text_types();
  259.     }
  260.  
  261.     my $clipboard = freshest_clipboard();
  262.     # Note: according to the GTK documentation, the wait_for_text method
  263.     # is supposed to always returns UTF-8. But it seems like the Perl
  264.     # bindings decode it and we get a string of chars instead of bytes.
  265.     my $content = $clipboard->wait_for_text;
  266.     my ($is_valid, $reason) = text_is_of_type($content, @valid_types);
  267.     return ($content) if $is_valid;
  268.     return (0, $reason);
  269. }
  270.  
  271. sub set_clipboards_text {
  272.     my $text = shift;
  273.     my $encoded_text = $encoding->encode($text);
  274.  
  275.     # Note: according to the GTK documentation, the set_text method
  276.     # is supposed to need input encoded in UTF-8. But it seems like the Perl
  277.     # bindings encode it, and we need to pass a string of chars instead of bytes.
  278.     foreach (all_clipboards()) {
  279.         $_->set_text($encoded_text);
  280.     }
  281.     # GTK fails setting the primary selection above, so let's use xclip :/
  282.     open(my $xclip, '| xclip') or die "Error opening pipe to xclip";
  283.     print $xclip $encoded_text or die "Error copying data to X clipboard";
  284.     close $xclip or die "Error closing pipe to xclip";
  285. }
  286.  
  287. sub get_status {
  288.     my $code = shift;
  289.     my $status;
  290.     my $trusted;
  291.     # Below taken from doc/DETAILS in GnuPG's sources
  292.     switch ($code) {
  293.         case "o" { $trusted = FALSE;
  294.                    $status = $encoding->decode(gettext("Unknown Trust")); }
  295.         case "-" { $trusted = FALSE;
  296.                     $status = $encoding->decode(gettext("Unknown Trust")); }
  297.         case "q" { $trusted = FALSE;
  298.                     $status = $encoding->decode(gettext("Unknown Trust")); }
  299.         case "m" { $trusted = FALSE;
  300.                     $status = $encoding->decode(gettext("Marginal Trust")); }
  301.         case "f" { $trusted = TRUE;
  302.                     $status = $encoding->decode(gettext("Full Trust")); }
  303.         case "u" { $trusted = TRUE;
  304.                     $status = $encoding->decode(gettext("Ultimate Trust")); }
  305.         else     { return; }
  306.     }
  307.     return ($status, $trusted);
  308. }
  309.  
  310. sub get_private_key_status {
  311.     my $key = shift;
  312.  
  313.     my $fingerprint = $encoding->decode($key->fingerprint->as_hex_string());
  314.     my $pubkey = ($gnupg->get_public_keys_light($fingerprint))[0]; # ignore collisions
  315.  
  316.     # a valid key may lack signing capabilities
  317.     return unless $pubkey->usage_flags =~ m/S/;
  318.  
  319.     my $validity = $pubkey->user_ids->[0]->validity;
  320.     return get_status($validity);
  321. }
  322.  
  323. sub get_public_key_status {
  324.     my $key = shift;
  325.  
  326.     # a valid key may lack encryption capabilities
  327.     return unless $key->usage_flags =~ m/E/;
  328.  
  329.     my $validity = $key->user_ids->[0]->validity;
  330.     return get_status($validity);
  331. }
  332.  
  333. sub create_key_row {
  334.     my $key = shift;
  335.  
  336.     my ($status, $trusted) = (ref($key) eq "GnuPG::SecretKey") ?
  337.         get_private_key_status($key)
  338.       : get_public_key_status($key);
  339.     # no status implies expired, revoked, etc. keys, which we don't want to list
  340.     return if !defined $status;
  341.  
  342.     my $name    = $encoding->decode($key->user_ids->[0]->as_string);
  343.     my $userids = join("\n", map { my $a = $_->as_string; my $b = $encoding->decode("$a"); my $c ="x" . "$b" }
  344.                                  $key->user_ids);
  345.     my $keyid   = $encoding->decode($key->short_hex_id);
  346.  
  347.     my $fingerprint = $encoding->decode($key->fingerprint->as_hex_string());
  348.     # Gtk2::SimpleList encodes these strings itself.
  349.     return [FALSE, $name, $keyid, $status, $fingerprint, $userids, $trusted];
  350. }
  351.  
  352. sub make_pub_key_list {
  353.     my $pub_keys_ref = shift;
  354.  
  355.     my $list = Gtk2::SimpleList->new (
  356.         ""                                    => 'bool', # C_SELECT
  357.         $encoding->decode(gettext("Name"))    => 'text', # C_NAME
  358.         $encoding->decode(gettext("Key ID"))  => 'text', # C_KEYID
  359.         $encoding->decode(gettext("Status"))  => 'text', # C_STATUS
  360.         ""                                    => 'text', # C_FINGERPRINT
  361.         ""                                    => 'text', # C_USERIDS
  362.         ""                                    => 'bool'  # C_TRUSTED
  363.         );
  364.     foreach my $i (VISIBLE_COLS) {
  365.         my $col = $list->get_column($i);
  366.         $col->set_max_width(400);
  367.         $col->set_resizable(TRUE);
  368.         $col->set_sort_column_id($i);
  369.     }
  370.     foreach my $i (HIDDEN_COLS) {
  371.         $list->get_column($i)->set_visible(FALSE);
  372.     }
  373.     $list->set_search_column(C_NAME);
  374.     $list->get_selection->set_mode('single');
  375.     $list->get_selection->unselect_all;
  376.     # Initially sort by name (couldn't find a cleaner way)
  377.     $list->get_column(C_NAME)->signal_emit('clicked');
  378.  
  379.     # If we used Gtk2::TreeView instead of Gtk2::SimpleList we could
  380.     # show all user ids directly in the list, but we make it simple
  381.     # for us and instead show them in the tooltip.
  382.     $list->set_has_tooltip(TRUE);
  383.     $list->signal_connect('query-tooltip' => sub {
  384.         my ($wx, $wy, $tooltip) = ($_[1], $_[2], $_[4]);
  385.         my ($x, $y) = $list->convert_widget_to_bin_window_coords($wx, $wy);
  386.         my $path = $list->get_path_at_pos($x, $y);
  387.         return FALSE unless defined $path;
  388.         my $row = ($path->get_indices)[0];
  389.         my $fingerprint =
  390.             join(" ", (${$list->{data}}[$row][C_FINGERPRINT] =~ m/..../g));
  391.         my $fingerprint_label = $encoding->decode(gettext("Fingerprint:"));
  392.         my $uids = "${$list->{data}}[$row][C_USERIDS]";
  393.         my $uids_label = $encoding->decode(
  394.             ngettext("User ID:", "User IDs:", ($uids =~ tr/\n//) + 1));
  395.         my $text = sprintf("%s\n%s\n%s\n%s", $uids_label, $uids,
  396.                            $fingerprint_label, $fingerprint);
  397.         $tooltip->set_text("$text");
  398.         return TRUE;
  399.     });
  400.  
  401.     $list->signal_connect('row-activated' => sub {
  402.         # Since we use 'single' selection mode, there can only be one
  403.         my $index = ($list->get_selected_indices)[0];
  404.         my $old_val = $list->{data}->[$index]->[C_SELECT];
  405.         $list->{data}->[$index]->[C_SELECT] = !$old_val;
  406.     });
  407.  
  408.     push @{$list->{data}},
  409.         grep { $_ } map { create_key_row($_) } @{$pub_keys_ref};
  410.  
  411.     $list->select(0);
  412.  
  413.     return $list;
  414. }
  415.  
  416. sub make_priv_key_combo {
  417.     my $priv_keys_ref = shift;
  418.  
  419.     my $list_store = Gtk2::ListStore->new(
  420.         qw/Glib::String Glib::String Glib::String Glib::String/);
  421.     my $iter = $list_store->append;
  422.     $list_store->set ($iter,
  423.                       COMBO_NAME, $encoding->decode(gettext(
  424.                                       "None (Don't sign)")),
  425.                       COMBO_KEYID, "",
  426.                       COMBO_FINGERPRINT, "",
  427.                       COMBO_ROLE, "none");
  428.     $iter = $list_store->append;
  429.     $list_store->set ($iter,
  430.                       COMBO_NAME, "",
  431.                       COMBO_KEYID, "",
  432.                       COMBO_FINGERPRINT, "",
  433.                       COMBO_ROLE, "separator");
  434.     foreach my $key (@{$priv_keys_ref}) {
  435.         my $row = create_key_row($key);
  436.         next unless $row; # skip keys without signing capability
  437.         $iter = $list_store->append;
  438.         $list_store->set ($iter,
  439.                           COMBO_NAME, "$row->[C_NAME]",
  440.                           COMBO_KEYID, "($row->[C_KEYID])",
  441.                           COMBO_FINGERPRINT, "$row->[C_FINGERPRINT]",
  442.                           COMBO_ROLE, "");
  443.     }
  444.  
  445.     my $sorted_list = Gtk2::TreeModelSort->new_with_model($list_store);
  446.     $sorted_list->set_default_sort_func(sub {
  447.         my ($model, $iter1, $iter2, $data) = @_;
  448.         my $name1 = $model->get($iter1, COMBO_NAME);
  449.         my $name2 = $model->get($iter2, COMBO_NAME);
  450.         my $role1 = $model->get($iter1, COMBO_ROLE);
  451.         my $role2 = $model->get($iter2, COMBO_ROLE);
  452.  
  453.         if ($role1 eq "none") {
  454.             return -1;
  455.         } elsif ($role2 eq "none") {
  456.             return 1;
  457.         } elsif ($role1 eq "separator") {
  458.             return -1;
  459.         } elsif ($role2 eq "separator") {
  460.             return 1;
  461.         } else {
  462.             return (lc $name1 cmp lc $name2);
  463.         }
  464.                                 });
  465.  
  466.     my $combo = Gtk2::ComboBox->new_with_model($sorted_list);
  467.     my $renderer = Gtk2::CellRendererText->new();
  468.     $combo->pack_start($renderer, FALSE);
  469.     $combo->add_attribute($renderer, 'text', COMBO_NAME);
  470.     $renderer = Gtk2::CellRendererText->new();
  471.     $combo->pack_start($renderer, FALSE);
  472.     $combo->add_attribute($renderer, 'text', COMBO_KEYID);
  473.     $combo->set_row_separator_func( sub {
  474.         my ($model, $iter, $data) = @_;
  475.         return TRUE if ($model->get($iter, COMBO_ROLE) eq "separator");
  476.                                         });
  477.     $combo->set_active(0);
  478.  
  479.     return $combo;
  480. }
  481.  
  482. sub choose_keys {
  483.     my $priv_keys_ref = shift;
  484.     my $pub_keys_ref = shift;
  485.  
  486.     my $pub_key_label = Gtk2::Label->new(
  487.         $encoding->decode(gettext("Select recipients:")));
  488.  
  489.     my $pub_key_list = make_pub_key_list($pub_keys_ref);
  490.     my $pub_key_list_scroll = Gtk2::ScrolledWindow->new;
  491.     $pub_key_list_scroll->set_policy('automatic', 'always');
  492.     $pub_key_list_scroll->add($pub_key_list);
  493.  
  494.     my $hide_recipients_checkbox = Gtk2::CheckButton->new(
  495.         $encoding->decode(gettext("Hide recipients")));
  496.     $hide_recipients_checkbox->set_has_tooltip(TRUE);
  497.     $hide_recipients_checkbox->set_tooltip_text(
  498.         $encoding->decode(gettext("Hide the user IDs of all recipients of " .
  499.                                   "an encrypted message. Otherwise anyone " .
  500.                                   "that sees the encrypted message can see " .
  501.                                   "who the recipients are.")));
  502.  
  503.     my $priv_key_label = Gtk2::Label->new(
  504.         $encoding->decode(gettext("Sign message as:")));
  505.  
  506.     my $priv_key_combo = make_priv_key_combo($priv_keys_ref);
  507.  
  508.     my $dialog = Gtk2::Dialog->new($encoding->decode(gettext("Choose keys")),
  509.                                    $main_window, 'destroy-with-parent',
  510.                                    'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok' );
  511.     $dialog->set_default_size(650,500);
  512.     $dialog->set_default_response('ok');
  513.     my $vbox = $dialog->get_content_area;
  514.     $vbox->pack_start($pub_key_label, FALSE, FALSE, 5);
  515.     $vbox->pack_start($pub_key_list_scroll, TRUE, TRUE, 0);
  516.     my $hbox = Gtk2::HBox->new;
  517.     $hbox->pack_start($priv_key_label, FALSE, FALSE, 0);
  518.     $hbox->pack_start($priv_key_combo, TRUE, TRUE, 0);
  519.     $vbox->pack_start($hbox, FALSE, FALSE, 5);
  520.     $vbox->pack_start($hide_recipients_checkbox, FALSE, FALSE, 0);
  521.  
  522.     $pub_key_list->grab_focus;
  523.     $dialog->show_all;
  524.  
  525.     $dialog->signal_connect('key-press-event' => sub {
  526.         my $event = $_[1];
  527.         return unless $event->keyval == $Gtk2::Gdk::Keysyms{Return};
  528.         $dialog->response('ok');
  529.         return 1;
  530.     });
  531.  
  532.     while ($dialog->run eq 'ok') {
  533.         my @recipients;
  534.         my $signer;
  535.         my $always_trust = 0;
  536.  
  537.         # Get signing key, if any
  538.         my $priv_key_combo_model = $priv_key_combo->get_model;
  539.         my $priv_key_iter = $priv_key_combo->get_active_iter;
  540.         $signer = $priv_key_combo_model->get($priv_key_iter, COMBO_FINGERPRINT);
  541.  
  542.         # Get public keys, if any
  543.         my @list_selection = grep { $_->[C_SELECT] } @{$pub_key_list->{data}};
  544.         if (@list_selection) {
  545.             my @unauth = grep { ! $_->[C_TRUSTED] } @list_selection;
  546.             if (@unauth) {
  547.                 my $title = $encoding->decode(gettext(
  548.                     "Do you trust these keys?"
  549.                                               ));
  550.                 my $warning = $encoding->decode(ngettext(
  551.                     "The following selected key is not fully trusted:",
  552.                     "The following selected keys are not fully trusted:",
  553.                     scalar @unauth
  554.                                                 ));
  555.                 my $msg = sprintf("%s\n", $warning);
  556.                 foreach my $key (@unauth) {
  557.                     # Each key will be listed RTL *or* LTR depending on the
  558.                     # direction of the first character of the name. This
  559.                     # unfortunately causes mixing of LTR and RTL in the
  560.                     # same list. A potential FIXME would be to display this
  561.                     # with a custom windows using SimpleList for the keys.
  562.                     # Also note that everything in $key (which originates
  563.                     # from $pub_key_list) already has been decoded, so we
  564.                     # don't have to do it again here.
  565.                     my $key_name = "$key->[C_NAME] ($key->[C_KEYID])";
  566.                     $msg = sprintf("%s%s\n", $msg, $key_name);
  567.                 }
  568.                 my $question = $encoding->decode(ngettext(
  569.                     "Do you trust this key enough to use it anyway?",
  570.                     "Do you trust these keys enough to use them anyway?",
  571.                     scalar @unauth
  572.                                                  ));
  573.                 $msg = sprintf("%s%s", $msg, $question);
  574.                 next unless display_question($dialog, $title, $msg);
  575.                 $always_trust = 1;
  576.             }
  577.             @recipients = map { $_->[C_FINGERPRINT] } @list_selection;
  578.         }
  579.  
  580.         if (!@recipients && !$signer) {
  581.             display_error($dialog,
  582.                           $encoding->decode(gettext("No keys selected")),
  583.                           $encoding->decode(gettext(
  584.                               "You must select a private key to sign the " .
  585.                               "message, or some public keys to encrypt the " .
  586.                               "message, or both."
  587.                                             )));
  588.             next;
  589.         }
  590.  
  591.         $dialog->destroy;
  592.         return {
  593.             always_trust => $always_trust,
  594.             hide_recipients => $hide_recipients_checkbox->get_active,
  595.             signer => $signer,
  596.             recipients => \@recipients,
  597.         };
  598.     }
  599.     $dialog->destroy;
  600.     return ();
  601. }
  602.  
  603. sub public_crypto {
  604.     my $args    = shift;
  605.     my $handles = $args->{handles};
  606.  
  607.     my @priv_keys = $gnupg->get_secret_keys_light();
  608.     my @pub_keys = $gnupg->get_public_keys_light();
  609.  
  610.     if (@priv_keys == 0 && @pub_keys == 0) {
  611.         display_error($main_window,
  612.                       $encoding->decode(gettext("No keys available")),
  613.                       $encoding->decode(gettext(
  614.                           "You need a private key to sign messages or a " .
  615.                           "public key to encrypt messages."
  616.                                        )));
  617.         return 0;
  618.     }
  619.  
  620.     my $chosen = choose_keys(\@priv_keys, \@pub_keys);
  621.     my $signer = $chosen->{signer};
  622.     my $recipients_ref = $chosen->{recipients};
  623.     my @recipients = @{$recipients_ref} if defined $recipients_ref;
  624.     my $always_trust = $chosen->{always_trust};
  625.     my $hide_recipients = $chosen->{hide_recipients};
  626.  
  627.     $gnupg->options->always_trust($always_trust);
  628.     $gnupg->options->clear_extra_args;
  629.     $gnupg->options->clear_meta_signing_key_id;
  630.     $gnupg->options->clear_recipients();
  631.  
  632.     if ($signer) {
  633.         $gnupg->options->meta_signing_key_id($signer);
  634.     }
  635.  
  636.     if (@recipients) {
  637.         if ($hide_recipients) {
  638.             # Since gpg's --no-throw-keyids seems to be broken (it doesn't
  639.             # work via the CLI either) we can't just push it to extra_args :/.
  640.             foreach my $recipient (@recipients) {
  641.                 $gnupg->options->push_extra_args('--hidden-recipient',
  642.                                                  $recipient);
  643.             }
  644.         } else {
  645.             $gnupg->options->push_recipients(@recipients);
  646.         }
  647.     }
  648.  
  649.     my $result = 0;
  650.  
  651.     if ($signer && !@recipients) {
  652.         $result = $gnupg->clearsign(handles => $handles);
  653.     } elsif (@recipients && !$signer) {
  654.         $result = $gnupg->encrypt(handles => $handles);
  655.     } elsif ($signer && @recipients) {
  656.         $result = $gnupg->sign_and_encrypt(handles => $handles);
  657.     }
  658.  
  659.     $gnupg->options->always_trust(0);
  660.     $gnupg->options->clear_extra_args;
  661.     $gnupg->options->clear_meta_signing_key_id;
  662.     $gnupg->options->clear_recipients();
  663.  
  664.     return $result;
  665. }
  666.  
  667. sub symmetric_encrypt {
  668.     my $args    = shift;
  669.     my $handles = $args->{handles};
  670.  
  671.     return $gnupg->encrypt_symmetrically(handles => $handles);
  672. }
  673.  
  674. sub decrypt_verify {
  675.     my $args    = shift;
  676.     my $handles = $args->{handles};
  677.     my $input   = $args->{input};
  678.  
  679.     my $text_type = detect_text_type($input);
  680.     return
  681.         $text_type eq 'message'
  682.       ? $gnupg->decrypt(handles => $handles)
  683.       : $gnupg->verify(handles => $handles);
  684. }
  685.  
  686. sub gpg_operate_on_text {
  687.     my $operation = shift;
  688.     my $text      = shift;
  689.  
  690.     $gnupg->options->hash_init(%gnupg_options);
  691.     my $in_h    = IO::Handle->new();
  692.     my $err_h   = IO::Handle->new();
  693.     my $out_h   = IO::Handle->new();
  694.     my $handles = GnuPG::Handles->new(
  695.         stdin => $in_h,
  696.         stderr => $err_h,
  697.         stdout => $out_h
  698.     );
  699.  
  700.     my $args = {
  701.         handles => $handles,
  702.         input   => $text,
  703.         in_h    => $in_h,
  704.         err_h   => $err_h,
  705.         out_h   => $out_h,
  706.     };
  707.  
  708.     my $pid = $operation->($args) or return;
  709.  
  710.     # We assume the sender/recipient uses the same charset as us :/
  711.     # PGP/MIME was invented for a reason.
  712.     print $in_h $encoding->encode($text);
  713.     close $in_h;
  714.  
  715.     my ($err, $out) = read_err_out($err_h, $out_h);
  716.     my @raw_stderr = @{$err};
  717.     my @raw_stdout = @{$out};
  718.  
  719.     waitpid $pid, 0; # Clean up the finished GnuPG process.
  720.  
  721.     my $std_err = $encoding->decode(join('', @raw_stderr));
  722.     my $std_out = $encoding->decode(join('', @raw_stdout));
  723.  
  724.     if ($CHILD_ERROR == 0) {
  725.         if ($operation eq \&decrypt_verify) {
  726.             my $msg;
  727.             if ($text =~ m/$pgp_signed_msg->{header}/) {
  728.                 $msg = $text;
  729.                 $msg =~ s/^.*$pgp_signed_msg->{header}\nHash: [^\n]*\n\n//m;
  730.                 $msg =~ s/^$pgp_signed_msg->{middle}.*//ms;
  731.             } else {
  732.                 $msg = $std_out;
  733.             }
  734.             display_output($msg, $std_err);
  735.         } else {
  736.             set_clipboards_text($std_out);
  737.         }
  738.     }
  739.     else {
  740.         display_error(
  741.             $main_window,
  742.             $encoding->decode(gettext("GnuPG error")),
  743.             $std_out . "\n\n" . $std_err
  744.         );
  745.         return;
  746.     }
  747.  
  748.     return;
  749. }
  750.  
  751. sub operate_on_clipboard {
  752.     my $operation   = shift;
  753.     my $valid_types = shift;
  754.  
  755.     my ($text, $clip_error) = get_validated_clipboard_text(
  756.         { valid_types => $valid_types }
  757.     );
  758.  
  759.     if (defined $clip_error) {
  760.         display_error(
  761.             $main_window,
  762.             $clip_error, # already translated and decoded
  763.             $encoding->decode(gettext("Therefore the operation cannot be " .
  764.                                       "performed."))
  765.         );
  766.         return;
  767.     }
  768.  
  769.     gpg_operate_on_text($operation, $text);
  770. }
  771.  
  772. sub display_error {
  773.     my $parent = shift;
  774.     my $title = shift;
  775.     my $msg   = shift;
  776.  
  777.     my $dialog = Gtk2::MessageDialog->new(
  778.         $parent, 'destroy-with-parent', 'error', 'ok',
  779.         $title
  780.     );
  781.     $dialog->format_secondary_text($msg);
  782.     $dialog->signal_connect(
  783.         response => sub { my $self = shift; $self->destroy; }
  784.     );
  785.     $dialog->set_position('center');
  786.     $dialog->run;
  787.     $dialog->destroy;
  788.  
  789.     return 1;
  790. }
  791.  
  792. sub display_question {
  793.     my $parent = shift;
  794.     my $title = shift;
  795.     my $msg   = shift;
  796.  
  797.     my $dialog = Gtk2::MessageDialog->new(
  798.         $parent, 'destroy-with-parent', 'question', 'yes-no', $title);
  799.     $dialog->format_secondary_text($msg);
  800.     $dialog->set_position('center');
  801.     my $answer = $dialog->run;
  802.     $dialog->destroy;
  803.     return $answer eq 'yes' ? TRUE : FALSE;
  804. }
  805.  
  806. # FIXME: let window grow depending on output text size
  807. sub display_output {
  808.     my $std_out = shift;
  809.     my $std_err = shift;
  810.  
  811.     my $dialog = Gtk2::MessageDialog->new(
  812.         $main_window, 'destroy-with-parent', 'info', 'ok',
  813.         $encoding->decode(gettext("GnuPG results"))
  814.     );
  815.     my $my_width_request = 800;
  816.     my $my_height_request = 600;
  817.     # TRANSLATORS: GnuPG stdout (encrypted or decrypted message)
  818.     $dialog->format_secondary_text(sprintf($encoding->decode(gettext(
  819.         "Output of GnuPG:"
  820.     ))));
  821.  
  822.     my $msg_area = $dialog->get_content_area;
  823.  
  824.     my $outbuf = Gtk2::TextBuffer->new();
  825.     $outbuf->set_text($std_out);
  826.     my $text_desc = Pango::FontDescription->new;
  827.     $text_desc->set_family('Monospace');
  828.     my $textview_out = Gtk2::TextView->new_with_buffer($outbuf);
  829.     $textview_out->set_editable(FALSE);
  830.     $textview_out->set_cursor_visible(FALSE);
  831.     $textview_out->set_left_margin(10);
  832.     $textview_out->set_right_margin(10);
  833.     $textview_out->set_wrap_mode('word');
  834.     $textview_out->modify_font($text_desc);
  835.     my $scrolled_win_out = Gtk2::ScrolledWindow->new;
  836.     $scrolled_win_out->set_policy('automatic', 'automatic');
  837.     $scrolled_win_out->add($textview_out);
  838.     $msg_area->pack_start($scrolled_win_out, TRUE, TRUE, 0);
  839.  
  840.     if (defined $std_err && length($std_err)) {
  841.         my $std_err_title = Gtk2::Label->new(
  842.             # TRANSLATORS: GnuPG stderr (other informational messages)
  843.             $encoding->decode(gettext(
  844.                 "Other messages provided by GnuPG:"
  845.             )));
  846.         $std_err_title->set_alignment(0, 0);
  847.         $std_err_title->set_padding(10, 0);
  848.         $msg_area->pack_start($std_err_title, FALSE, FALSE, 0);
  849.         my $std_err_buf = Gtk2::TextBuffer->new();
  850.         $std_err_buf->set_text($std_err);
  851.         my $textview_err = Gtk2::TextView->new_with_buffer($std_err_buf);
  852.         $textview_err->set_editable(FALSE);
  853.         $textview_err->set_cursor_visible(FALSE);
  854.         $textview_err->set_left_margin(10);
  855.         $textview_err->set_right_margin(10);
  856.         $textview_err->set_wrap_mode('word');
  857.         $textview_err->modify_font($text_desc);
  858.         my $scrolled_win_err = Gtk2::ScrolledWindow->new;
  859.         $scrolled_win_err->set_policy('automatic', 'automatic');
  860.         $scrolled_win_err->add($textview_err);
  861.         $scrolled_win_err->set_size_request(-1, $my_height_request/5);
  862.         $msg_area->pack_start($scrolled_win_err, FALSE, FALSE, 0);
  863.     }
  864.  
  865.     $dialog->signal_connect(
  866.         response => sub { my $self = shift; $self->destroy; }
  867.     );
  868.     my $screen_width = $dialog->get_screen()->get_width();
  869.     my $screen_height = $dialog->get_screen()->get_height();
  870.     if ($screen_width > $my_width_request || $screen_height > $my_height_request) {
  871.         $dialog->set_size_request($my_width_request, $my_height_request);
  872.     } else {
  873.         $dialog->maximize();
  874.     }
  875.     $dialog->set_resizable(TRUE);
  876.     $dialog->set_position('center');
  877.     $dialog->show_all;
  878.  
  879.     return 1;
  880. }
  881.  
  882. # Read stdout and stderr at the same time, one line at a time, to
  883. # avoid dead-locking due to one of the buffers being full.
  884. sub read_err_out {
  885.     my $err_h = shift;
  886.     my $out_h = shift;
  887.  
  888.     my $err   = [];
  889.     my $out   = [];
  890.  
  891.     while (1) {
  892.         my $err_l = <$err_h>;
  893.         my $out_l = <$out_h>;
  894.         push @{$err}, $err_l if defined $err_l;
  895.         push @{$out}, $out_l if defined $out_l;
  896.         last unless ($err_l || $out_l);
  897.     }
  898.     close $err_h;
  899.     close $out_h;
  900.  
  901.     return ($err, $out);
  902. }
  903.  
  904. sub update_icon {
  905.     my $text_type = shift;
  906.  
  907.     $statusicon->set_from_stock("gpgApplet-${text_type}");
  908. }
  909.  
  910. sub detect_received {
  911.     my $clipboard = shift;
  912.  
  913.     update_icon(detect_text_type(get_validated_clipboard_text()));
  914. }
  915.  
  916. sub handle_clipboard_owner_change {
  917.     my $clipboard = shift;
  918.  
  919.     set_freshest_clipboard($clipboard);
  920.     detect_received($clipboard);
  921. }
  922.  
  923. sub make_icon_source {
  924.     my $icon = shift;
  925.     my $base = shift;
  926.     my $ext  = shift;
  927.     my $size = shift;
  928.  
  929.     my $filename = "/usr/share/pixmaps/gpgApplet/$base/$icon.$ext";
  930.     my $source = Gtk2::IconSource->new();
  931.     $source->set_filename($filename);
  932.     $source->set_direction_wildcarded(1);
  933.     $source->set_state_wildcarded(1);
  934.     if (defined $size) {
  935.         $source->set_size_wildcarded(0);
  936.         $source->set_size($size);
  937.     } else {
  938.         $source->set_size_wildcarded(1);
  939.     }
  940.  
  941.     return $source;
  942. }
  943.  
  944. sub init_icons_stock {
  945.     my $factory = shift;
  946.  
  947.     $factory->add_default;
  948.     my @stock_ids = map { "gpgApplet-$_" } qw{ message none signed text };
  949.  
  950.     foreach my $stock_id (@stock_ids) {
  951.         my $iconset = Gtk2::IconSet->new();
  952.         $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'button'));
  953.         $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'menu'));
  954.         $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'large-toolbar'));
  955.         $iconset->add_source(make_icon_source($stock_id, "22x22",    "png", 'small-toolbar'));
  956.         $iconset->add_source(make_icon_source($stock_id, "48x48",    "png", 'dialog'));
  957.         $iconset->add_source(make_icon_source($stock_id, "scalable", "svg"));
  958.         $factory->add($stock_id, $iconset);
  959.     }
  960. }
  961.